package org.dromara.common.ratelimiter.aspectj;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.dromara.common.core.constant.GlobalConstants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MessageUtils;
import org.dromara.common.core.utils.ServletUtils;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.ratelimiter.annotation.RateLimiter;
import org.dromara.common.ratelimiter.enums.LimitType;
import org.dromara.common.redis.utils.RedisUtils;
import org.redisson.api.RateType;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;

import java.lang.reflect.Method;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：限流处理
 */
@Slf4j
@Aspect
public class RateLimiterAspect {

    /**
     * 定义spel表达式解析器
     */
    private final ExpressionParser parser = new SpelExpressionParser();

    /**
     * 定义spel解析模版
     */
    private final ParserContext parserContext = new TemplateParserContext();

    /**
     * 方法参数解析器
     */
    private final ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();

    /**
     * 在执行目标方法之前进行限流控制
     * 该方法通过解析RateLimiter注解来获取限流参数，并根据这些参数在Redis中实现限流逻辑
     * 如果超过限流标准，则抛出ServiceException异常
     *
     * @param point       当前执行的连接点（方法）
     * @param rateLimiter 方法上的RateLimiter注解实例
     * @throws ServiceException 如果达到限流标准，则抛出此异常
     * @throws RuntimeException 如果Redis操作异常，则抛出此异常
     */
    @Before("@annotation(rateLimiter)")
    public void doBefore(JoinPoint point, RateLimiter rateLimiter) {
        // 从RateLimiter注解中获取限流时间和数量
        int time = rateLimiter.time();
        int count = rateLimiter.count();
        try {
            // 构建组合键，用于Redis中唯一标识此次限流控制
            String combineKey = getCombineKey(rateLimiter, point);
            // 默认限流类型为总体限制
            RateType rateType = RateType.OVERALL;
            // 如果限流范围是集群，则改为每个客户端单独限制
            if (rateLimiter.limitType() == LimitType.CLUSTER) {
                rateType = RateType.PER_CLIENT;
            }
            // 使用Redis实现限流，返回剩余的请求次数
            long number = RedisUtils.rateLimiter(combineKey, rateType, count, time);
            // 如果返回-1，表示超过限流标准
            if (number == -1) {
                // 获取超限提示信息
                String message = rateLimiter.message();
                // 如果消息格式符合特定的模板，则进行格式化
                if (StringUtils.startsWith(message, "{") && StringUtils.endsWith(message, "}")) {
                    message = MessageUtils.message(StringUtils.substring(message, 1, message.length() - 1));
                }
                // 抛出异常，提示达到限流标准
                throw new ServiceException(message);
            }
            // 日志记录限流信息：发放的令牌数、剩余令牌数、缓存Key
            log.info("限制令牌 => {}, 剩余令牌 => {}, 缓存key => '{}'", count, number, combineKey);
        } catch (Exception e) {
            // 如果是ServiceException异常，则直接抛出
            if (e instanceof ServiceException) {
                throw e;
            } else {
                // 如果是其他异常，则包装为RuntimeException后抛出
                throw new RuntimeException("服务器限流异常，请稍候再试", e);
            }
        }
    }

    /**
     * 根据注解和方法参数生成限流器的唯一键
     *
     * @param rateLimiter 限流器注解
     * @param point 切点对象，用于获取方法和参数信息
     * @return 生成的限流器键
     */
    private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
        // 从注解中获取键表达式
        String key = rateLimiter.key();
        // 如果键表达式不为空
        if (StringUtils.isNotBlank(key)) {
            // 获取方法签名
            MethodSignature signature = (MethodSignature) point.getSignature();
            // 获取目标方法
            Method targetMethod = signature.getMethod();
            // 获取方法参数
            Object[] args = point.getArgs();
            // 创建方法上下文，用于解析键表达式
            //noinspection DataFlowIssue
            MethodBasedEvaluationContext context =
                new MethodBasedEvaluationContext(null, targetMethod, args, pnd);
            // 设置Bean解析器，以便表达式能解析Spring容器中的Bean
            context.setBeanResolver(new BeanFactoryResolver(SpringUtils.getBeanFactory()));
            // 创建表达式对象
            Expression expression;
            // 如果键表达式以特定前缀和后缀开始和结束，使用带有上下文信息的解析器解析
            if (StringUtils.startsWith(key, parserContext.getExpressionPrefix())
                && StringUtils.endsWith(key, parserContext.getExpressionSuffix())) {
                expression = parser.parseExpression(key, parserContext);
            } else {
                // 否则直接解析表达式
                expression = parser.parseExpression(key);
            }
            // 解析键表达式并获取最终的键值
            key = expression.getValue(context, String.class);
        }
        // 创建StringBuilder对象，用于构建最终的限流器键
        StringBuilder stringBuffer = new StringBuilder(GlobalConstants.RATE_LIMIT_KEY);
        // 将请求的URI添加到键中
        stringBuffer.append(ServletUtils.getRequest().getRequestURI()).append(":");
        // 根据限流类型添加相应的信息到键中
        if (rateLimiter.limitType() == LimitType.IP) {
            // 获取请求ip
            // 如果限流类型为IP，添加客户端IP到键中
            stringBuffer.append(ServletUtils.getClientIP()).append(":");
        } else if (rateLimiter.limitType() == LimitType.CLUSTER) {
            // 获取客户端实例id
            // 如果限流类型为集群，添加客户端实例ID到键中
            stringBuffer.append(RedisUtils.getClient().getId()).append(":");
        }
        // 返回构建完成的限流器键
        return stringBuffer.append(key).toString();
    }
}
